--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit f7e6c98ed0b6ca96e9c33b0969f9b895be0b3307
Parents : 389034d
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-16T16:33:32-05:00
feat: add missed call count to Calls nav icon and update related UI components
Changes
19 files changed, 126 insertions(+), 5 deletions(-)
Diff
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 297bf85e..fec15f20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,6 +48,7 @@ All notable changes to this project will be documented in this file.
- Unknown meshchatx links return a clear error instead of falling through to LXMF
- NomadNet Micron copy no longer inserts a newline between every character
- Unread message count is a red pill on the Messages nav icon
+- Missed call count is a red pill on the Calls nav icon
- Notification bell removed from the header
- Unread badge stays circular and remains visible when the sidebar is collapsed
- Open conversations mark as read when a new message arrives without needing to reselect the thread
diff --git a/docs/agents/conventions/frontend.md b/docs/agents/conventions/frontend.md
index a65f9a70..e34ddae7 100644
--- a/docs/agents/conventions/frontend.md
+++ b/docs/agents/conventions/frontend.md
@@ -6,6 +6,7 @@ Applies when editing `meshchatx/src/frontend/**/*.{vue,js}`.
- API calls go through `window.api` (not ad-hoc axios imports in pages).
- Toasts: `ToastUtils.success|error|warning|info|loading|dismiss`.
- New top-level pages need: route in `main.js`, nav entry when discoverable, `en.json` keys, frontend tests.
+- Sidebar unread pills live on nav entries in `coreNavEntries.js` and counters in `GlobalState`. Do not bring back a header notification bell for that job.
- Do not use `_`-prefixed keys in Vue `data()` (`vue/no-reserved-keys`).
- File inputs: prefer broad `accept` for identity keys (`.bin,.key,.identity,application/octet-stream,*/*`). Database restore stays `.zip`.
- Prefer existing MaterialDesignIcon / layout patterns over new design systems.
diff --git a/docs/agents/overview.md b/docs/agents/overview.md
index 5130a4a6..5db2e6fb 100644
--- a/docs/agents/overview.md
+++ b/docs/agents/overview.md
@@ -312,7 +312,7 @@ meshchatx --restore-db /path/to/backup.zip
Before finishing work in these areas, verify the matching invariants:
1. **Identity import / tutorial** - key-only vs zip restore copy is correct, picker accepts real exports, activate-on-finish / skip paths do not orphan imports.
-2. **Conversations / notifications DB** - slim queries, MEMORY temp under Landlock, 503 on retryable SQLite errors.
+2. **Conversations / notifications DB** - slim queries, MEMORY temp under Landlock, 503 on retryable SQLite errors. Sidebar unread pills (messages, relay mentions, missed calls) stay in sync with read/viewed state. No header notification bell.
3. **Auth / CSRF / WS config** - no new unauthenticated mutating surfaces, no sensitive settings over open WS mutators.
4. **Plugins** - permissions declared, install preview/consent preserved, signatures/integrity not bypassed.
5. **Android bridges** - MIME mapping, storage paths, and WebView navigation guards remain correct.
diff --git a/docs/agents/skills/contribution-registries/SKILL.md b/docs/agents/skills/contribution-registries/SKILL.md
index b3124d34..9089b52a 100644
--- a/docs/agents/skills/contribution-registries/SKILL.md
+++ b/docs/agents/skills/contribution-registries/SKILL.md
@@ -7,6 +7,7 @@ Wire nav, tools, commands, settings search, and WebSocket events through registr
- Adding a discoverable page, tool, command palette entry, or settings section
- Adding a new WebSocket event type handled by the UI
- Plugin contribution points or slot UI
+- Adding or changing a sidebar nav badge (unread counts)
## Registries
@@ -20,6 +21,28 @@ Wire nav, tools, commands, settings search, and WebSocket events through registr
Core boot registers once via `registerCoreContributions.js` and `core*Entries.js` siblings.
+## Nav badges
+
+Sidebar pills are declared on `CORE_NAV_ENTRIES` in `coreNavEntries.js` via `badge: { source, pill, cap }`.
+
+Current sources:
+
+| Source | Meaning | Cleared when |
+| -------------------------- | ------------------------------------------- | ------------------------------------ |
+| `unreadConversationsCount` | Unread LXMF conversations | Conversation marked read |
+| `relayChatUnreadCount` | RRC mention count (when relay chat enabled) | Mentions consumed on Relay chat page |
+| `missedCallsCount` | Unviewed `telephone_missed_call` rows | Call page opened or history cleared |
+
+`App.vue` maps each source through `GlobalState` and `getNavBadgeCount`. Collapsed sidebar still shows pill badges on the icon. There is no header notification bell anymore. Message sounds stay under Settings.
+
+For a new badge:
+
+1. Add a `GlobalState` counter
+2. Add a `NavBadgeSource` value and `badge` on the nav entry
+3. Wire `getNavBadgeCount` in `App.vue`
+4. Refresh the count from the right API or WebSocket event
+5. Clear it when the user has actually seen the related UI
+
## Hard rules
- New top-level pages still need a route in `main.js` (see `page-toast-tests`). Registries cover discoverability and dispatch, not routing alone.
@@ -31,6 +54,8 @@ Core boot registers once via `registerCoreContributions.js` and `core*Entries.js
- `meshchatx/src/frontend/js/registries/`
- `meshchatx/src/frontend/js/registries/registerCoreContributions.js`
+- `meshchatx/src/frontend/js/registries/coreNavEntries.js`
+- `meshchatx/src/frontend/js/GlobalState.js`
- `meshchatx/src/frontend/components/plugins/PluginSlotNode.vue`
- `meshchatx/src/frontend/main.js` (routes)
diff --git a/docs/en/audio-calls.md b/docs/en/audio-calls.md
index c2ce38ce..1c935145 100644
--- a/docs/en/audio-calls.md
+++ b/docs/en/audio-calls.md
@@ -37,6 +37,8 @@ Voicemail events surface as `new_voicemail` on the WebSocket.
The **Call** area keeps history of placed, received, and missed calls. You can record calls when the feature is enabled and policy allows storage on your device.
+Unread missed calls show as a red count on the Calls sidebar icon and the header phone button. Opening the Call page clears that count. Desktop and Android still show a one-shot missed-call notification when the event happens.
+
## Ringtones
Upload custom ringtones and assign them per contact. Default sounds are used when no override exists.
diff --git a/docs/en/getting-started.md b/docs/en/getting-started.md
index 441c8f07..1eb409b1 100644
--- a/docs/en/getting-started.md
+++ b/docs/en/getting-started.md
@@ -63,7 +63,7 @@ The same backend code powers Docker images, Python wheels, Linux packages, Elect
| Identities | `/identities` | Create, import, or switch identities |
| Documentation | `/documentation` | MeshChatX guides and the Reticulum manual |
-Relay chat appears only when `rrc_enabled` is turned on in settings.
+Relay chat appears only when `rrc_enabled` is turned on in settings. When enabled, the Relay chat icon can show a red mention count. Messages shows unread conversation count. Calls shows unread missed-call count.
## Documentation in the app
diff --git a/docs/en/messaging.md b/docs/en/messaging.md
index d54688de..64bd5054 100644
--- a/docs/en/messaging.md
+++ b/docs/en/messaging.md
@@ -6,6 +6,8 @@ MeshChatX uses LXMF (LXMF Message Format) for direct and store-and-forward messa
Open **Messages** to see your conversation list. Each row is a peer destination you have exchanged traffic with or selected from announces.
+The Messages sidebar icon shows a red count when you have unread LXMF conversations. Opening a thread marks it read. If a new message arrives while that thread is already open, it is marked read without needing to switch away and back.
+
From a conversation you can:
- Send and receive text messages
diff --git a/meshchatx.rsm b/meshchatx.rsm
index 1e273517..ce7d0f52 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ
diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 7db40ffc..509f9ae6 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -10209,10 +10209,19 @@ class ReticulumMeshChat:
async def telephone_status(request):
# make sure telephone is enabled
if self.telephone_manager.telephone is None:
+ missed_calls_unread_count = 0
+ if self.database is not None:
+ with contextlib.suppress(Exception):
+ missed_calls_unread_count = (
+ self.database.misc.get_unread_notification_count_by_type(
+ "telephone_missed_call",
+ )
+ )
return web.json_response(
{
"enabled": False,
"message": "Telephone is disabled",
+ "missed_calls_unread_count": missed_calls_unread_count,
},
)
@@ -10374,6 +10383,9 @@ class ReticulumMeshChat:
"active_call": active_call,
"is_mic_muted": self.telephone_manager.transmit_muted,
"is_speaker_muted": self.telephone_manager.receive_muted,
+ "missed_calls_unread_count": self.database.misc.get_unread_notification_count_by_type(
+ "telephone_missed_call",
+ ),
"voicemail": {
"is_recording": self.voicemail_manager.is_recording,
"unread_count": self.database.voicemails.get_unread_count(),
@@ -10415,6 +10427,17 @@ class ReticulumMeshChat:
},
)
+ @routes.post("/api/v1/telephone/missed-calls/mark-viewed")
+ async def telephone_missed_calls_mark_viewed(request):
+ not_ready = self._require_identity_context_ready()
+ if not_ready is not None:
+ return not_ready
+ await asyncio.to_thread(
+ self.database.misc.dismiss_unviewed_notifications,
+ "telephone_missed_call",
+ )
+ return web.json_response({"message": "ok"})
+
# answer incoming telephone call
@routes.get("/api/v1/telephone/answer")
async def telephone_answer(request):
diff --git a/meshchatx/src/backend/database/misc.py b/meshchatx/src/backend/database/misc.py
index eb28ec04..4f789704 100644
--- a/meshchatx/src/backend/database/misc.py
+++ b/meshchatx/src/backend/database/misc.py
@@ -304,6 +304,16 @@ class MiscDAO:
)
return row["count"] if row else 0
+ def get_unread_notification_count_by_type(self, notification_type):
+ if not notification_type:
+ return 0
+ row = self.provider.fetchone(
+ "SELECT COUNT(*) as count FROM notifications "
+ "WHERE is_viewed = 0 AND type = ?",
+ (notification_type,),
+ )
+ return row["count"] if row else 0
+
# Keyboard Shortcuts
def get_keyboard_shortcuts(self, identity_hash):
return self.provider.fetchall(
diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index b31a6a56..c2083f95 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -122,11 +122,17 @@
</button>
<button
type="button"
- class="rounded-full p-1.5 sm:p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 transition-colors"
+ class="relative rounded-full p-1.5 sm:p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 transition-colors"
:title="$t('app.audio_calls')"
@click="$router.push({ name: 'call' })"
>
<MaterialDesignIcon icon-name="phone" class="w-5 h-5 sm:w-6 sm:h-6" />
+ <span
+ v-if="missedCallsCount > 0"
+ class="absolute -top-0.5 -right-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-bold leading-none text-white"
+ >
+ {{ missedCallsCount > 99 ? "99+" : missedCallsCount }}
+ </span>
</button>
<button
type="button"
@@ -731,6 +737,9 @@ export default {
relayChatUnreadCount() {
return GlobalState.relayChatUnreadCount;
},
+ missedCallsCount() {
+ return GlobalState.missedCallsCount;
+ },
rrcEnabled() {
return GlobalState.config?.rrc_enabled !== false;
},
@@ -907,6 +916,9 @@ export default {
if (item.badge.source === "relayChatUnreadCount") {
return this.relayChatUnreadCount;
}
+ if (item.badge.source === "missedCallsCount") {
+ return this.missedCallsCount;
+ }
return 0;
},
formatNavBadgeCount(item) {
@@ -1242,10 +1254,12 @@ export default {
ToastUtils.success(this.$t("identities.switched"));
GlobalState.unreadConversationsCount = 0;
+ GlobalState.missedCallsCount = 0;
await this.getConfig();
await this.updateRingtonePlayer();
await this.getAppInfo();
+ this.updateTelephoneStatus();
this.isSwitchingIdentity = false;
@@ -1409,6 +1423,7 @@ export default {
NotificationUtils.showMissedCallNotification(
json.remote_identity_name || json.remote_identity_hash
);
+ this.updateTelephoneStatus();
},
telephone_initiation_status: (json) => {
this.initiationStatus = json.status;
@@ -1926,6 +1941,7 @@ export default {
this.initiationStatus = response.data.initiation_status;
this.initiationTargetHash = response.data.initiation_target_hash;
this.initiationTargetName = response.data.initiation_target_name;
+ GlobalState.missedCallsCount = response.data?.missed_calls_unread_count ?? 0;
// Update call ended state if needed
const justEnded = oldCall != null && this.activeCall == null;
diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index 8bed093e..06f924d3 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -2566,9 +2566,11 @@ export default {
this.getVoicemailStatus();
this.getRingtones();
this.getRingtoneStatus();
+ this.markMissedCallsViewed();
GlobalEmitter.on("telephone-history-updated", this.getHistory);
GlobalEmitter.on("telephone-history-updated", this.getVoicemails);
+ GlobalEmitter.on("telephone-history-updated", this.markMissedCallsViewed);
// poll for status
this.statusInterval = setInterval(() => {
@@ -2604,6 +2606,7 @@ export default {
beforeUnmount() {
GlobalEmitter.off("telephone-history-updated", this.getHistory);
GlobalEmitter.off("telephone-history-updated", this.getVoicemails);
+ GlobalEmitter.off("telephone-history-updated", this.markMissedCallsViewed);
if (this.statusInterval) clearInterval(this.statusInterval);
if (this.historyInterval) clearInterval(this.historyInterval);
@@ -3742,12 +3745,21 @@ export default {
try {
await window.api.delete("/api/v1/telephone/history");
this.callHistory = [];
+ await this.markMissedCallsViewed();
ToastUtils.success(this.$t("call.call_history_cleared"));
} catch (e) {
console.error(e);
ToastUtils.error(this.$t("call.failed_to_clear_call_history"));
}
},
+ async markMissedCallsViewed() {
+ try {
+ await window.api.post("/api/v1/telephone/missed-calls/mark-viewed");
+ GlobalState.missedCallsCount = 0;
+ } catch (e) {
+ console.error(e);
+ }
+ },
async blockIdentity(hash) {
if (!confirm(`Are you sure you want to banish this identity?`)) return;
try {
diff --git a/meshchatx/src/frontend/js/GlobalState.js b/meshchatx/src/frontend/js/GlobalState.js
index 063e5726..6de2c69d 100644
--- a/meshchatx/src/frontend/js/GlobalState.js
+++ b/meshchatx/src/frontend/js/GlobalState.js
@@ -11,6 +11,7 @@ const globalState = reactive({
messageTimestampGroupingEnabled: true,
unreadConversationsCount: 0,
relayChatUnreadCount: 0,
+ missedCallsCount: 0,
activeCallTab: "phone",
blockedDestinations: [],
modifiedInterfaceNames: new Set(),
diff --git a/meshchatx/src/frontend/js/registries/coreNavEntries.js b/meshchatx/src/frontend/js/registries/coreNavEntries.js
index 3d8d5c02..51c4e59b 100644
--- a/meshchatx/src/frontend/js/registries/coreNavEntries.js
+++ b/meshchatx/src/frontend/js/registries/coreNavEntries.js
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: 0BSD
-/** @typedef {'unreadConversationsCount' | 'relayChatUnreadCount'} NavBadgeSource */
+/** @typedef {'unreadConversationsCount' | 'relayChatUnreadCount' | 'missedCallsCount'} NavBadgeSource */
/**
* @typedef {Object} NavEntry
@@ -28,6 +28,7 @@ export const CORE_NAV_ENTRIES = [
route: { name: "call" },
icon: "phone",
labelKey: "app.audio_calls",
+ badge: { source: "missedCallsCount", pill: true, cap: 99 },
},
{
id: "contacts",
diff --git a/tests/backend/http_api_response_registry.py b/tests/backend/http_api_response_registry.py
index 7be54859..0af6c93b 100644
--- a/tests/backend/http_api_response_registry.py
+++ b/tests/backend/http_api_response_registry.py
@@ -482,6 +482,11 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
),
HttpJsonContract("GET", "/api/v1/telephone/status", TELEPHONE_STATUS_SCHEMA),
HttpJsonContract("GET", "/api/v1/telephone/history", TELEPHONE_HISTORY_SCHEMA),
+ HttpJsonContract(
+ "POST",
+ "/api/v1/telephone/missed-calls/mark-viewed",
+ {"type": "object", "additionalProperties": True},
+ ),
HttpJsonContract(
"GET",
"/api/v1/telephone/recordings",
diff --git a/tests/backend/test_notifications.py b/tests/backend/test_notifications.py
index 90bcf2b9..566a4b5e 100644
--- a/tests/backend/test_notifications.py
+++ b/tests/backend/test_notifications.py
@@ -444,6 +444,18 @@ def test_random_notification_operations(db, operations):
assert count >= 0
+def test_unread_notification_count_by_type(db):
+ db.misc.add_notification("telephone_missed_call", "h1", "Missed Call", "c1")
+ db.misc.add_notification("telephone_missed_call", "h2", "Missed Call", "c2")
+ db.misc.add_notification("other", "h3", "Other", "c3")
+ assert db.misc.get_unread_notification_count_by_type("telephone_missed_call") == 2
+ assert db.misc.get_unread_notification_count_by_type("other") == 1
+ assert db.misc.get_unread_notification_count_by_type("missing") == 0
+ db.misc.dismiss_unviewed_notifications("telephone_missed_call")
+ assert db.misc.get_unread_notification_count_by_type("telephone_missed_call") == 0
+ assert db.misc.get_unread_notification_count_by_type("other") == 1
+
+
@pytest.mark.asyncio
@pytest.mark.usefixtures("require_loopback_tcp")
async def test_auth_middleware_returns_401_for_api_without_session_when_auth_enabled(
diff --git a/tests/frontend/AppIdentitySwitch.test.js b/tests/frontend/AppIdentitySwitch.test.js
index 39d0f591..27689f09 100644
--- a/tests/frontend/AppIdentitySwitch.test.js
+++ b/tests/frontend/AppIdentitySwitch.test.js
@@ -29,6 +29,7 @@ function makeCtx() {
getConfig: vi.fn().mockResolvedValue(undefined),
updateRingtonePlayer: vi.fn().mockResolvedValue(undefined),
getAppInfo: vi.fn().mockResolvedValue(undefined),
+ updateTelephoneStatus: vi.fn(),
$t: (key) => key,
};
}
@@ -37,10 +38,12 @@ describe("App.vue applyIdentitySwitched", () => {
beforeEach(() => {
vi.clearAllMocks();
GlobalState.unreadConversationsCount = 3;
+ GlobalState.missedCallsCount = 2;
});
afterEach(() => {
GlobalState.unreadConversationsCount = 0;
+ GlobalState.missedCallsCount = 0;
vi.useRealTimers();
});
@@ -52,9 +55,11 @@ describe("App.vue applyIdentitySwitched", () => {
});
expect(ToastUtils.success).toHaveBeenCalledWith("identities.switched");
expect(GlobalState.unreadConversationsCount).toBe(0);
+ expect(GlobalState.missedCallsCount).toBe(0);
expect(ctx.getConfig).toHaveBeenCalledTimes(1);
expect(ctx.updateRingtonePlayer).toHaveBeenCalledTimes(1);
expect(ctx.getAppInfo).toHaveBeenCalledTimes(1);
+ expect(ctx.updateTelephoneStatus).toHaveBeenCalledTimes(1);
expect(ctx.isSwitchingIdentity).toBe(false);
expect(GlobalEmitter.emit).toHaveBeenCalledWith(
"identity-switched",
diff --git a/tests/frontend/__snapshots__/uiSnapshots.test.js.snap b/tests/frontend/__snapshots__/uiSnapshots.test.js.snap
index 7d468f6a..e592e006 100644
--- a/tests/frontend/__snapshots__/uiSnapshots.test.js.snap
+++ b/tests/frontend/__snapshots__/uiSnapshots.test.js.snap
@@ -8,7 +8,7 @@ exports[`UI snapshot regression > FormSubLabel.vue > default sub-label 1`] = `"<
exports[`UI snapshot regression > IconButton.vue > icon button 1`] = `"<button type="button" class="text-gray-500 hover:text-gray-700 dark:text-zinc-400 dark:hover:text-zinc-100 hover:bg-gray-100 dark:hover:bg-zinc-800 p-2 rounded-full w-9 h-9 flex items-center justify-center shrink-0 transition-all duration-200"><span class="icon">+</span></button>"`;
-exports[`UI snapshot regression > SidebarLink.vue > sidebar link 1`] = `"<a class="router-link-stub" href="#" custom=""><a href="#" type="button" class="hover:bg-gray-100 dark:hover:bg-zinc-700 w-full text-gray-800 dark:text-zinc-200 group flex gap-x-3 rounded-r-full p-2 mr-2 text-sm leading-6 font-semibold focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:focus-visible:outline-zinc-500 overflow-hidden"><span class="my-auto shrink-0"><span class="icon-slot">M</span></span><span class="my-auto flex w-full truncate transition-all duration-300">Messages</span></a></a>"`;
+exports[`UI snapshot regression > SidebarLink.vue > sidebar link 1`] = `"<a class="router-link-stub" href="#" custom=""><a href="#" type="button" class="hover:bg-gray-100 dark:hover:bg-zinc-700 overflow-hidden w-full text-gray-800 dark:text-zinc-200 group flex gap-x-3 rounded-r-full p-2 mr-2 text-sm leading-6 font-semibold focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:focus-visible:outline-zinc-500"><span class="my-auto shrink-0"><span class="icon-slot">M</span></span><span class="my-auto flex w-full truncate transition-all duration-300">Messages</span></a></a>"`;
exports[`UI snapshot regression > Toast.vue > empty state 1`] = `"<div="" class="fixed max-sm:bottom-[calc(1rem+env(safe-area-inset-bottom,0px))] bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:right-4 sm:translate-x-0 z-100 flex flex-col gap-2 pointer-events-none w-[calc(100%-2rem)] max-w-sm sm:w-auto sm:max-w-md"><div="" class="snapshot-transition-group" name="toast"></div></div>"`;
diff --git a/tests/frontend/registries.test.js b/tests/frontend/registries.test.js
index c2f8b18f..8e31f6ac 100644
--- a/tests/frontend/registries.test.js
+++ b/tests/frontend/registries.test.js
@@ -116,4 +116,9 @@ describe("registerCoreContributions", () => {
expect(listNavItems()).toHaveLength(CORE_NAV_ENTRIES.length);
expect(listTools()).toHaveLength(CORE_TOOLS_ENTRIES.length);
});
+
+ it("calls nav entry has a missed-calls pill badge", () => {
+ const call = CORE_NAV_ENTRIES.find((entry) => entry.id === "call");
+ expect(call?.badge).toEqual({ source: "missedCallsCount", pill: true, cap: 99 });
+ });
});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────